package org.jing.core.util;

import org.jing.core.lang.ExceptionHandler;
import org.jing.core.lang.JingException;

import java.io.BufferedInputStream;
import java.io.BufferedReader;
import java.io.ByteArrayOutputStream;
import java.io.File;
import java.io.FileInputStream;
import java.io.FileOutputStream;
import java.io.InputStream;
import java.io.InputStreamReader;
import java.io.OutputStream;
import java.math.BigDecimal;
import java.math.RoundingMode;
import java.nio.channels.FileChannel;
import java.nio.charset.Charset;
import java.nio.file.Files;
import java.util.ArrayList;
import java.util.Enumeration;
import java.util.List;
import java.util.function.BiConsumer;
import java.util.zip.ZipEntry;
import java.util.zip.ZipFile;
import java.util.zip.ZipInputStream;
import java.util.zip.ZipOutputStream;

/**
 * Description: <br>
 *
 * @author bks <br>
 * @since 2019-01-09 <br>
 */
@SuppressWarnings({ "WeakerAccess", "unused" })
public class FileUtil {
    public static final String SYSTEM_LINE_SEPARATOR = System.lineSeparator();

    /**
     * Description: 检查文件是否存在. <br>
     * 必须是<b>文件</b>, 而不是<b>文件夹</b>. <br>
     *
     * @param file 待检查文件对象 <br>
     * @return <br>
     */
    public static boolean checkFileExist(File file) {
        return null != file && file.exists() && file.isFile();
    }

    /**
     * Description: 检查文件是否存在. <br>
     * 必须是<b>文件</b>, 而不是<b>文件夹</b>. <br>
     *
     * @param filePath 待检查文件路径 <br>
     * @return <br>
     */
    public static boolean checkFileExist(String filePath) {
        return checkFileExist(new File(filePath));
    }

    /**
     * Description: 检查文件夹是否存在. <br>
     * 必须是<b>文件夹</b>, 而不是<b>文件</b>. <br>
     *
     * @param directory 待检查文件夹对象 <br>
     * @return <br>
     */
    public static boolean checkDirectoryExist(File directory) {
        return null != directory && directory.exists() && directory.isDirectory();
    }

    /**
     * Description: 检查文件夹是否存在. <br>
     * 必须是<b>文件夹</b>, 而不是<b>文件</b>. <br>
     *
     * @param directoryPath 待检查文件夹路径 <br>
     * @return <br>
     */
    public static boolean checkDirectoryExist(String directoryPath) {
        return checkDirectoryExist(new File(directoryPath));
    }

    /**
     * Description: 按照指定编码获取文件的BufferedReader. <br>
     * 适用于可被编码影响的文件类型, 比如.txt文件. <br>
     *
     * @param file 文件 <br>
     * @param encoding 编码格式 <br>
     * @return <br>
     */
    public static BufferedReader getBufferedReader(File file, String encoding) {
        ExceptionHandler.publishIfMatch(StringUtil.isEmpty(encoding), "encoding required");
        try {
            return new BufferedReader(new InputStreamReader(Files.newInputStream(file.toPath()), encoding));
        }
        catch (Throwable t) {
            throw new JingException(t, "failed to get reader [{}]", file.getAbsolutePath());
        }
    }

    /**
     * Description: 获取资源文件的输入流. <br>
     * 一般用于和classpath相关的场景. <br>
     *
     * @param resourcePath 资源文件路径 <br>
     * @return <br>
     */
    public static InputStream getResourceInputStream(String resourcePath) {
        try {
            ClassLoader[] classLoader = ClassUtil.getClassLoader(FileUtil.class);
            int size = GenericUtil.count(classLoader);
            for (int i$ = 0; i$ < size; i$++) {
                ClassLoader classLoader$ = classLoader[i$];
                InputStream is;
                if (null != classLoader$) {
                    is = classLoader$.getResourceAsStream(resourcePath);
                    if (null == is) {
                        is = classLoader$.getResourceAsStream(File.separator + resourcePath);
                    }
                    if (null != is) {
                        return is;
                    }
                }
            }
            throw new JingException("no class loader or stream available for [{}]", resourcePath);
        }
        catch (Throwable t) {
            throw new JingException(t, "failed To readFile resource [{}]", resourcePath);
        }
    }

    /**
     * Description: zip压缩文件. <br>
     *
     * @param zipFilePath 输出zip文件路径 <br>
     * @param srcFilePath 待压缩文件路径 <br>
     */
    public static void zip(String zipFilePath, String srcFilePath) {
        zip(zipFilePath, srcFilePath, false);
    }

    /**
     * Description: zip压缩文件. <br>
     * 可指定是否带上根目录文件夹 <br>
     *
     * @param zipFilePath 输出zip文件路径 <br>
     * @param srcFilePath 待压缩文件路径 <br>
     * @param skipParentDirectory 是否跳过根目录文件夹 <br>
     */
    public static void zip(String zipFilePath, String srcFilePath, boolean skipParentDirectory) {
        File zipFile = new File(zipFilePath);
        if (zipFile.exists() && !zipFile.delete()) {
            throw new JingException("failed to clear the file archive [{}]", zipFile.getAbsolutePath());
        }
        try (ZipOutputStream out = new ZipOutputStream(Files.newOutputStream(zipFile.toPath()))) {
            File sourceFile = new File(srcFilePath);
            if (skipParentDirectory && !sourceFile.isDirectory()) {
                throw new JingException("src file must be directory");
            }
            String base = sourceFile.getName();
            if (skipParentDirectory) {
                File[] fileList = sourceFile.listFiles();
                int count = GenericUtil.count(fileList);
                for (int i$ = 0; i$ < count; i$++) {
                    assert fileList != null;
                    zipSubFunc(out, fileList[i$], fileList[i$].getName());
                }
            }
            else {
                zipSubFunc(out, sourceFile, base);
            }
        }
        catch (Throwable t) {
            throw new JingException(t);
        }
    }

    private static void zipSubFunc(ZipOutputStream out, File srcFile, String base) throws Exception {
        if (srcFile.isDirectory()) {
            File[] fileList = srcFile.listFiles();
            int count = GenericUtil.count(fileList);
            if (count == 0) {
                out.putNextEntry(new ZipEntry(base + File.separator));
            }
            else {
                for (int i$ = 0; i$ < count; i$++) {
                    assert fileList != null;
                    zipSubFunc(out, fileList[i$], base + File.separator + fileList[i$].getName());
                }
            }
        }
        else {
            out.putNextEntry(new ZipEntry(base));
            try (
                FileInputStream fos = new FileInputStream(srcFile);
                BufferedInputStream bis = new BufferedInputStream(fos)
            ) {
                int tag;
                while ((tag = bis.read()) != -1) {
                    out.write(tag);
                }
            }
        }
    }

    /**
     * Description: 解压zip文件. <br>
     *
     * @param zipFile 待解压zip文件 <br>
     */
    public static List<ZipEntry> unzipFile(ZipFile zipFile) {
        List<ZipEntry> retList = new ArrayList<>();
        Enumeration<? extends ZipEntry> zipFiles = zipFile.entries();
        ZipEntry zipEntry;
        while (zipFiles.hasMoreElements()) {
            zipEntry = zipFiles.nextElement();
            retList.add(zipEntry);
        }
        return retList;
    }

    /**
     * Description: 解压zip文件. <br>
     *
     * @param is 输入流 <br>
     * @param charset 字符集 <br>
     * @param destDir 目标文件夹 <br>
     */
    public static void unzipFile(InputStream is, String charset, String destDir) {
        List<ZipEntry> retList = new ArrayList<>();
        File dir = new File(destDir);
        mkdirs(dir);
        try (ZipInputStream zis = new ZipInputStream(is, Charset.forName(charset))) {
            ZipEntry zipEntry;
            byte[] buffer = new byte[1024];
            while (null != (zipEntry = zis.getNextEntry())) {
                File file = new File(dir, zipEntry.getName());
                if (!zipEntry.isDirectory()) {
                    mkdirs(file.getAbsoluteFile().getParentFile());
                    try (FileOutputStream fos = new FileOutputStream(file)) {
                        int len;
                        while ((len = zis.read(buffer)) > 0) {
                            fos.write(buffer, 0, len);
                        }
                    }
                }
                else {
                    mkdirs(dir);
                }
            }
        }
        catch (Throwable t) {
            throw new JingException(t, "failed to unzip file");
        }
    }

    /**
     * Description: 解压zip文件. <br>
     *
     * @param is 输入流 <br>
     * @param charset 字符集 <br>
     * @param action 循环体 <br>
     */
    public static void unzipFile(InputStream is, String charset, BiConsumer<ZipEntry, byte[]> action) {
        List<ZipEntry> retList = new ArrayList<>();
        try (ZipInputStream zis = new ZipInputStream(is, Charset.forName(charset))) {
            ZipEntry zipEntry;
            byte[] buffer = new byte[1024];
            while (null != (zipEntry = zis.getNextEntry())) {
                if (!zipEntry.isDirectory()) {
                    try (ByteArrayOutputStream bos = new ByteArrayOutputStream()) {
                        int len;
                        while ((len = zis.read(buffer)) != -1) {
                            bos.write(buffer, 0, len);
                        }
                        action.accept(zipEntry, bos.toByteArray());
                    }
                }
                else {
                    action.accept(zipEntry, null);
                }
            }
        }
        catch (Throwable t) {
            throw new JingException(t, "failed to unzip file");
        }
    }

    /**
     * Description: 通过nio通道复制文件. <br>
     *
     * @param srcFile 源文件 <br>
     * @param destFile 目标文件 <br>
     */
    public static void copyFileByChannel(File srcFile, File destFile) {
        try (
            FileInputStream fis = new FileInputStream(srcFile);
            FileOutputStream fos = new FileOutputStream(destFile);
            FileChannel srcChannel =  fis.getChannel();
            FileChannel destChannel = fos.getChannel()
        ) {
            destChannel.transferFrom(srcChannel, 0, srcChannel.size());
        }
        catch (Throwable t) {
            throw new JingException(t);
        }
    }

    /**
     * Description: 通过nio通道复制文件. <br>
     *
     * @param srcFilePath 源文件路径 <br>
     * @param destFilePath 目标文件路径 <br>
     */
    public static void copyFileByChannel(String srcFilePath, String destFilePath) {
        copyFileByChannel(new File(srcFilePath), new File(destFilePath));
    }

    /**
     * Description: 通过字节流复制文件. <br>
     *
     * @param srcFile 源文件 <br>
     * @param destFile 目标文件 <br>
     * @param bufferSize 字节流缓冲区大小 <br>
     */
    public static void copyFileByStream(File srcFile, File destFile, int bufferSize) {
        try (
            InputStream in = Files.newInputStream(srcFile.toPath());
            OutputStream out = Files.newOutputStream(destFile.toPath())
        ) {
            byte[] buffer = new byte[bufferSize];
            int len;
            while ((len = in.read(buffer)) > 0) {
                out.write(buffer, 0, len);
            }
        }
        catch (Throwable t) {
            throw new JingException(t);
        }
    }

    /**
     * Description: 通过字节流复制文件. <br>
     *
     * @param srcFilePath 源文件路径 <br>
     * @param destFilePath 目标文件路径 <br>
     * @param bufferSize 字节流缓冲区大小 <br>
     */
    public static void copyFileByStream(String srcFilePath, String destFilePath, int bufferSize) {
        copyFileByStream(new File(srcFilePath), new File(destFilePath), bufferSize);
    }

    /**
     * Description: 通过字节流复制文件. <br>
     * 缓冲区大小为<b>3 * 1024(b)</b>. <br>
     *
     * @param srcFile 源文件 <br>
     * @param destFile 目标文件 <br>
     */
    public static void copyFileByStream(File srcFile, File destFile) {
        copyFileByStream(srcFile, destFile, 3 * 1024);
    }

    /**
     * Description: 通过字节流复制文件. <br>
     * 缓冲区大小为<b>3 * 1024(b)</b>. <br>
     *
     * @param srcFilePath 源文件路径 <br>
     * @param destFilePath 目标文件路径 <br>
     */
    public static void copyFileByStream(String srcFilePath, String destFilePath) {
        copyFileByStream(srcFilePath, destFilePath, 3 * 1024);
    }

    /**
     * Description: 删除文件. <br>
     * 如果是文件夹还会递归删除. <br>
     *
     * @param file 待删除文件 <br>
     * @return 是否完全删除 <br>
     */
    public static boolean deleteFile(File file) {
        if (null == file || !file.exists()) {
            return true;
        }
        boolean flag = true;
        if (file.isDirectory()) {
            File[] files = file.listFiles();
            int size = null == files ? 0 : files.length;
            for (int i$ = 0; i$ < size; i$++) {
                flag = flag && deleteFile(files[i$]);
            }
        }
        flag = flag && file.delete();
        return flag;
    }

    /**
     * Description: 删除指定路径的文件. <br>
     * 如果是文件夹还会递归删除. <br>
     *
     * @param filePath 待删除文件的路径 <br>
     * @return 是否完全删除 <br>
     */
    public static boolean deleteFile(String filePath) {
        return deleteFile(new File(filePath));
    }

    /**
     * Description: 如果目录不存在则创建目录. <br>
     *
     * @param directory 目录 <br>
     * @return 目录已存在或是否创建成功 <br>
     */
    public static boolean mkdirs(File directory) {
        return checkDirectoryExist(directory) || directory.mkdirs();
    }

    /**
     * Description: 如果目录不存在则创建目录. <br>
     *
     * @param directoryPath 目录路径 <br>
     * @return 目录已存在或是否创建成功 <br>
     */
    public static boolean mkdirs(String directoryPath) {
        return mkdirs(new File(directoryPath));
    }

    /**
     *  Description: 构建适合当前OS的路径字符串, 入参里的路径分隔符应该是<b>?</b>. <br>
     *
     * @param path <br>
     * @return <br>
     */
    public static String buildPath(String path) {
        path = StringUtil.ifEmpty(path);
        int index;
        while ((index = path.indexOf("?")) != -1) {
            path = path.substring(0, index) + File.separator + path.substring(index + 1);
        }
        return path;
    }

    /**
     * Description: 获取文件名后缀. <br>
     *
     * @param file 文件 <br>
     * @return <br>
     */
    public static String getFileSuffix(File file) {
        return getFileSuffix(file.getName());
    }

    /**
     * Description: 获取文件名后缀. <br>
     *
     * @param fileName 文件名 <br>
     * @return <br>
     */
    public static String getFileSuffix(String fileName) {
        int potIndex = fileName.lastIndexOf(".");
        if (-1 != potIndex) {
            return fileName.substring(potIndex + 1);
        }
        return "";
    }

    /**
     * Description: 在指定目录下创建一个临时的文件夹 <br>
     *
     * @param path 父目录 <br>
     * @return <br>
     */
    public static File createTempDirectory(String path) {
        path = buildPath(path);
        int times = 0;
        while (true) {
            File dir = new File(path + (StringUtil.isEmpty(path) ? "" : File.separator) + DateUtil.getCurrentDateString("yyyyMMddHHmmssSSS") + String.format("%03d", times));
            if (dir.exists() && dir.isDirectory()) {
                times = (times + 1) % 1000;
            }
            else {
                mkdirs(dir);
                return dir;
            }
        }
    }

    /**
     * Description: 在指定目录下计算出一个新的临时文件 <br>
     *
     * @param path 父目录 <br>
     * @return <br>
     */
    public static File createTempFile(String path) {
        path = buildPath(path);
        int times = 0;
        while (true) {
            File dir = new File(path + (StringUtil.isEmpty(path) ? "" : File.separator) + DateUtil.getCurrentDateString("yyyyMMddHHmmssSSS") + String.format("%03d", times));
            if (dir.exists() && dir.isFile()) {
                times = (times + 1) % 1000;
            }
            else {
                mkdirs(dir.getAbsoluteFile().getParentFile());
                return dir;
            }
        }
    }

    /**
     * Description: 将路径组合起来 <br>
     *
     * @param paths 路径子项 <br>
     */
    public static String concatPath(String... paths) {
        StringBuilder stbr = new StringBuilder();
        GenericUtil.loop(paths, p$ -> {
            if (StringUtil.isEmpty(p$)) {
                return;
            }
            boolean endFlag = stbr.length() == 0 || stbr.charAt(stbr.length() - 1) == File.separatorChar;
            boolean startFlag = p$.charAt(0) == File.separatorChar;
            if (endFlag && startFlag) {
                stbr.append(p$.substring(1));
            }
            else if (endFlag && !startFlag || !endFlag && startFlag) {
                stbr.append(p$);
            }
            else {
                stbr.append(File.separatorChar).append(p$);
            }
        });
        return stbr.toString();
    }

    /**
     * Description: 根据文件字节大小获取文件大小的表达式 <br>
     * 精度为2 <br>
     *
     * @param size 字节大小 <br>
     * @return <br>
     */
    public static String getFileSize(long size) {
        return getFileSize(size, 2);
    }

    /**
     * Description: 根据文件字节大小获取文件大小的表达式 <br>
     *
     * @param size 字节大小 <br>
     * @param scale 精度 <br>
     * @return <br>
     */
    public static String getFileSize(long size, int scale) {
        final String[] units = {"B", "KB", "MB", "GB", "TB"};
        BigDecimal temp = BigDecimal.valueOf(size);
        BigDecimal rate = BigDecimal.valueOf(1024);
        int index = 0;
        while (size / Math.pow(1024, index) > 1024) {
            if (index == units.length - 1) {
                break;
            }
            index ++;
        }
        BigDecimal result = BigDecimal.valueOf(size).divide(BigDecimal.valueOf(Math.pow(1024, index)), scale, RoundingMode.HALF_UP);
        if (index == 0) {
            result = result.setScale(0, RoundingMode.HALF_UP);
        }
        return result.toPlainString() + units[index];
    }

    /**
     * Description: 根据文件大小的表达式计算字节大小 <br>
     *
     * @param size 文件大小 <br>
     * @return <br>
     */
    public static long parseFileSize(String size) {
        try {
            size = size.toUpperCase();
            String unit = size.substring(size.length() - 2).toUpperCase();
            String value = size.substring(0, size.length() - 2);
            int times = 0;
            switch (unit) {
                case "KB":
                    times = 1;
                    break;
                case "MB":
                    times = 2;
                    break;
                case "GB":
                    times = 3;
                    break;
                case "TB":
                    times = 4;
                    break;
                default:
                    return -1;
            }
            BigDecimal result = BigDecimal.valueOf(1024).pow(times).multiply(new BigDecimal(value));
            return result.setScale(0, RoundingMode.HALF_UP).longValue();
        }
        catch (Throwable t) {
            return -1;
        }
    }
}
